import _ from 'lodash';
import express from 'express';
import logger from '../../common/logger';
import asyncHandler from 'express-async-handler';

import { eventBus } from '../../domain/service/eventBus';
import { JobRetryEvent } from '../../domain/event/jobRetry';
import { JobFailedEvent } from '../../domain/event/jobFailed';
import { JobCreatedEvent } from '../../domain/event/jobCreated';
import { JobSucceedEvent } from '../../domain/event/jobSucceed';
import { JobIterruptedEvent } from '../../domain/event/jobInterrupted';
import { SimulationJobService } from '../../domain/model/simulation/service';
import { SimulationJobDto, SimulationJobPageQuery } from '../../domain/model/simulation/dtos';
import {
  JobContext,
  JobDefinition,
  SimJobStatus,
  SimJobType,
  SimulationResult,
  SimulationSpec
} from '../../domain/model/simulation/type';
import * as connectionManager from '../../domain/service/connectionManager';

interface ComponentJobRequest {
  context: JobContext;
  simServerIdentity: string;
  definition: JobDefinition;
}

export function init(app: express.Application) {
  app.get('/api/jobs', asyncHandler(async (req, res) => {
    const jobs = await new SimulationJobService().findAll();
    res.json({
      success: true,
      jobs
    });
  }));

  app.get('/api/jobs/:userId/:appId/:nodeId', asyncHandler(async (req, res) => {
    const { userId, appId, nodeId } = req.params;
    const jobs = await new SimulationJobService().findByContext(userId, appId, nodeId);
    const sortedjobs = _.orderBy(jobs, ['startedAt'], 'desc');

    res.json({
      success: true,
      jobs: sortedjobs
    });
  }));

  app.post('/api/jobs/:userId/:appId/:nodeId', asyncHandler(async (req, res) => {
    logger.info(`jobs api, request body = ${JSON.stringify(req.body, null, 2)}`);

    const { userId, appId, nodeId } = req.params;
    const { businessId, status } = req.body;

    let jobs = await new SimulationJobService().findByContext(userId, appId, nodeId);

    if (businessId) {
      jobs = jobs.filter(job => job.businessId === businessId);
    }

    if (status) {
      jobs = jobs.filter((job, index) => {
        if (status === 'queuing') {
          if (['created', 'agentAssigned', 'agentDispatched'].indexOf(job.status) > -1) {
            return true;
          }
          return false;
        } else if (status !== job.status) {
          return false;
        }
        return true;
      });
    }

    const sortedjobs = _.orderBy(jobs, ['startedAt'], 'desc');
    res.json({
      success: true,
      jobs: sortedjobs
    });
  }));

  // 分页查询仿真任务列表
  app.post('/api/jobs/page/:userId/:appId/:nodeId', asyncHandler(async (req, res) => {
    logger.info(`jobs api, request body = ${JSON.stringify(req.body, null, 2)}`);

    const { userId, appId, nodeId } = req.params;
    const simulationJobPageQuery: SimulationJobPageQuery = req.body;
    simulationJobPageQuery.userId = userId;
    simulationJobPageQuery.appId = appId;
    simulationJobPageQuery.nodeId = nodeId;
    let jobs = await new SimulationJobService().findForPage(simulationJobPageQuery);
    res.json({
      success: true,
      data: jobs
    });
  }));

  app.get('/api/jobs/:id', asyncHandler(async (req, res) => {
    const job = await new SimulationJobService().findById(_.toNumber(req.params['id']));
    if (job) {
      res.json({ success: true, job });
    } else {
      res.json({ success: false, message: 'job not found' });
    }
  }));

  // update job status from agent post
  app.post('/api/jobs/:id/status', asyncHandler(async (req, res) => {
    const jobId = req.params['id'];
    logger.info(`update job status from agent post, jobId: ${jobId}, request body = ${JSON.stringify(req.body, null, 2)}`);

    const status = req.body.status as SimJobStatus;
    const service = new SimulationJobService();
    const jobDto = await service.findById(jobId);

    if (jobDto) {
      const oldStatus = jobDto.status;
      if (status !== oldStatus) {
        jobDto.status = status;
        if (status === SimJobStatus.Running) {
          jobDto.startedAt = new Date();
        }
        await service.save(jobDto);

        if (jobDto.type === SimJobType.Sub && jobDto.parentId) {
          const parentJob = await service.findById(jobDto.parentId);
          if (parentJob) {
            parentJob.status = status;
            await service.save(parentJob);
          }
        }
      } 
      res.json({ success: true });
    } else {
      res.json({ success: false, message: `simulation job(id=${jobId}) not found` });
    }
  }));

  interface JobResultNotificationRequest {
    id: string,
    success: boolean,
    result: SimulationResult,
    message: string
  }
  // update job status from agent post
  app.post('/api/jobs/:id/result', asyncHandler(async (req, res) => {
    const jobId = req.params['id'];
    logger.info(`update job result from agent post, jobId: ${jobId}`);

    const { success, result, message } = req.body as JobResultNotificationRequest;

    const service = new SimulationJobService();
    const simulationJobDto = await service.findById(_.toNumber(jobId));

    if (simulationJobDto) {
      const preSimStatus = simulationJobDto.status;
      if (preSimStatus === SimJobStatus.Succeed || preSimStatus === SimJobStatus.Failed) {
        logger.warn(`update job result failed, pre sim job status is ${preSimStatus}`);
        res.json({ success: true });
        return;
      }

      simulationJobDto.status = success ? SimJobStatus.Succeed : SimJobStatus.Failed;
      simulationJobDto.result = result;
      simulationJobDto.message = message;

      simulationJobDto.finishedAt = new Date();
      if (!_.isEmpty(simulationJobDto.progress)) {
        simulationJobDto.progress = 100;
      }
      await service.save(simulationJobDto);

      if (simulationJobDto.type === SimJobType.Sub && simulationJobDto.parentId) {
        const parentJob = await service.findById(simulationJobDto.parentId);
        if (parentJob && parentJob.subJobIds) {
          let parentJobFinished = true;
          for (const subJobId of parentJob.subJobIds) {
            if (_.toNumber(subJobId) === simulationJobDto.id) {
              continue;
            }
            const otherSubJob = await service.findById(subJobId);
            if (![SimJobStatus.Succeed, SimJobStatus.Failed, SimJobStatus.Interrupt].includes(otherSubJob.status)) {
              parentJobFinished = false;
              break;
            }
          }
          if (parentJobFinished) {
            parentJob.status = SimJobStatus.Succeed;
            await service.save(parentJob);
          }
        }
      }

      res.json({ success: true });

      // if (success) {
      //   eventBus.emit(JobSucceedEvent.NAME, new JobSucceedEvent(simulationJobDto));
      // } else {
      //   eventBus.emit(JobFailedEvent.NAME, new JobFailedEvent(simulationJobDto));
      // }
    } else {
      res.json({
        success: false,
        message: `simulation job(id=${jobId}) was not tracked by gateway`
      });
      logger.warn(`simulation job(id=${jobId}) was not tracked by gateway`);
    }
  }));

  // create job from node
  app.post('/api/jobs', asyncHandler(async (req, res) => {
    logger.info(`create job from node, request body = ${JSON.stringify(req.body, null, 2)}`);
    const { context, simServerIdentity, definition } = req.body as ComponentJobRequest;

    const simulationJobDto = {} as SimulationJobDto;
    simulationJobDto.product = definition.product;
    simulationJobDto.businessId = definition.businessId;
    simulationJobDto.desc = definition.desc;
    simulationJobDto.spec = definition.spec;

    simulationJobDto.userId = context.userId;
    simulationJobDto.appId = context.appId;
    simulationJobDto.nodeId = context.nodeId;
    simulationJobDto.debug = context.debug;
    simulationJobDto.notificationCallback = context.notificationCallback;

    simulationJobDto.agentId = simServerIdentity;
    simulationJobDto.status = SimJobStatus.Created;
    simulationJobDto.submitAt = new Date();

    simulationJobDto.id = await new SimulationJobService().save(simulationJobDto);

    res.json({ success: true, jobId: simulationJobDto.id });

    eventBus.emit(JobCreatedEvent.NAME, new JobCreatedEvent(simulationJobDto));
  }));

  app.post('/api/jobs/multiJob', asyncHandler(async (req, res) => {
    logger.info(`create multi job from node, request body = ${JSON.stringify(req.body, null, 2)}`);
    const { subJobIds, userId, appId, nodeId, businessId } = req.body;
    const simulationJobDto = {} as SimulationJobDto;
    simulationJobDto.product = 'multiSolvers';
    simulationJobDto.spec = {} as SimulationSpec;
    simulationJobDto.type = SimJobType.Multi;
    simulationJobDto.subJobIds = subJobIds;
    simulationJobDto.submitAt = new Date();
    simulationJobDto.status = SimJobStatus.Created;
    simulationJobDto.userId = userId;
    simulationJobDto.appId = appId;
    simulationJobDto.nodeId = nodeId;
    simulationJobDto.businessId = businessId;
    const simulationJobService = new SimulationJobService();
    simulationJobDto.id = await simulationJobService.save(simulationJobDto);

    for (const subJobId of subJobIds) {
      await simulationJobService.updateSubJobByJobId(_.toNumber(subJobId), simulationJobDto.id);
    }
    res.json({ success: true, jobId: simulationJobDto.id });
  }));


  app.post('/api/job/stop/:userId/:appId/:nodeId/:businessId', asyncHandler((async (req, res) => {
    const { userId, appId, nodeId, businessId } = req.params;

    const simulationJobService = new SimulationJobService();

    const job = await simulationJobService.findByContextAndBusinessId(userId, appId, nodeId, businessId);
    
    if (!job) {
      res.json({
        success: false,
        message: `businessId: ${businessId} is not tracked.`
      });
    } else {
      if (job.status in [SimJobStatus.Created, SimJobStatus.AgentAssigned]) {
        job.status = SimJobStatus.Interrupt;
        await new SimulationJobService().save(job);
      }
      res.json({ success: true, businessId });
      eventBus.emit(JobIterruptedEvent.NAME, new JobIterruptedEvent(job));
    }
  })));

  app.post('/api/job/stop/:jobId', asyncHandler((async (req, res) => {
    const simulationJobService = new SimulationJobService();
    const jobId = req.params.jobId;
    const { modelBack } = req.body;
    const job = await simulationJobService.findById(jobId);

    if (!job) {
      res.json({
        success: false,
        message: `jobId: ${jobId} is not tracked.`
      });
    } else {
      job.status = SimJobStatus.Interrupt;
      if (!_.isEmpty(job.progress)) {
        job.progress = 100;
      }
      await new SimulationJobService().save(job);
      res.json({ success: true, jobId });
      eventBus.emit(JobIterruptedEvent.NAME, new JobIterruptedEvent(job), modelBack || false);
    }
  })));

  // 暂停任务
  app.get('/api/job/pause/:jobId', asyncHandler((async (req, res) => {
    res.json({ success: true });
  })));

  // 批量删除任务
  app.post('/api/job/delete/batch/:userId/:appId/:nodeId', asyncHandler((async (req, res) => {
    logger.info(`delete job batch request: ${JSON.stringify(req.body, null, 2)}`);
    const { userId, appId, nodeId } = req.params;
    const { businessIds } = req.body;

    if (!_.isEmpty(businessIds)) {
      const deletedJobIds = await new SimulationJobService().deleteByContextAndBusinessIds(userId, appId, nodeId, businessIds);
      res.json({ success: true, jobIds: deletedJobIds });

    } else {
      res.json({ success: false, message: 'businessIds is empty.' });
    }
  })));

  app.post('/api/job/delete/batch', asyncHandler((async (req, res) => {
    logger.info(`delete job batch request: ${JSON.stringify(req.body, null, 2)}`);
    const { jobIds } = req.body;

    if (!_.isEmpty(jobIds)) {
      const deletedJobIds = await new SimulationJobService().deleteByIds(jobIds);
      res.json({ success: true, jobIds: deletedJobIds });

    } else {
      res.json({ success: false, message: 'businessIds is empty.' });
    }
  })));

  app.post('/api/job/retry/:userId/:appId/:nodeId/:businessId', asyncHandler((async (req, res) => {
    const { userId, appId, nodeId, businessId } = req.params;
    const job = await new SimulationJobService().findByContextAndBusinessId(userId, appId, nodeId, businessId);
    if (job) {
      job.status = SimJobStatus.Created;
      job.submitAt = new Date();
      job.startedAt = null;
      job.finishedAt = null;
      job.progress = null;
      job.message = null;
      job.result = null;
      await new SimulationJobService().save(job);
      eventBus.emit(JobRetryEvent.NAME, new JobRetryEvent(job));
      res.json({ success: true });
    } else {
      res.json({
        success: false,
        message: 'job is not existed'
      });
    }
  })));

  // 任务重试
  app.post('/api/job/retry/:jobId', asyncHandler((async (req, res) => {
    const job = await new SimulationJobService().findById(req.params.jobId);
    if (job) {
      const preJobAgentId = job.agentId;
      const connection = connectionManager.getConnection(preJobAgentId);

      if (connection) {
        job.submitAt = new Date();
        job.status = SimJobStatus.AgentAssigned;
        job.startedAt = null;
        job.finishedAt = null;
        job.progress = null;
        job.message = null;
        job.result = null;
        await new SimulationJobService().save(job);
        eventBus.emit(JobRetryEvent.NAME, new JobRetryEvent(job));
        res.json({ success: true });
      } else {
        res.json({ success: false, message: `仿真服务器${preJobAgentId}不在线,重试失败` });
      }
    } else {
      res.json({
        success: false,
        message: 'job is not existed'
      });
    }
  })));

  app.post('/api/job/progress/:jobId', asyncHandler((async (req, res) => {
    const { progress } = req.body;
    const simJobService = new SimulationJobService();
    const job = await simJobService.findById(req.params.jobId);
    if (job && job.status === SimJobStatus.Running) {
      await simJobService.updateProgressByJobId(_.toNumber(req.params.jobId), progress);
    }
    res.json({ success: true });
  })));

  app.post('/api/job/stopJobByNode/:nodeId', asyncHandler((async (req, res) => {
    logger.info(`stop sim job by node: ${req.params.nodeId}`);
    const simJobService = new SimulationJobService();
    const jobs = await simJobService.findByNodeIdAndStatuses(req.params.nodeId, [SimJobStatus.Running, SimJobStatus.AgentDispatched, SimJobStatus.AgentAssigned]);
    if (!_.isEmpty(jobs)) {
      for (const job of jobs) {
        eventBus.emit(JobIterruptedEvent.NAME, job);
        job.status = SimJobStatus.Failed;
        job.message = 'sim job failed when stop app manually';
        if (job.progress) {
          job.progress = 100;
        }
        await simJobService.save(job);
      }
    }
    res.json({ success: true, data: true });
  })));

  // 获取并轨任务详情
  app.get('/api/multiJob/:jobId', asyncHandler((async (req, res) => {
    logger.info(`api multi job: ${req.params.jobId}`);
    const simJobService = new SimulationJobService();
    const parentJob = await simJobService.findById(req.params.jobId);
    const subJobs = [];
    if (parentJob && parentJob.subJobIds) {
      for (const subJobId of parentJob.subJobIds) {
        const subJob = await simJobService.findById(subJobId);
        subJobs.push(subJob);
      }
    }
    res.json({ success: true, data: subJobs });
  })));

  app.get('/api/multiJob/detail/:businessId', asyncHandler((async (req, res) => {
    logger.info(`api multi job detail: ${req.params.businessId}`);
    const simJobService = new SimulationJobService();
    const parentJob = await simJobService.getLatestByBusinessIdAndProduct(req.params.businessId, 'multiSolvers');
    const subJobs = [];
    if (parentJob && parentJob.subJobIds) {
      for (const subJobId of parentJob.subJobIds) {
        const subJob = await simJobService.findById(subJobId);
        subJobs.push(subJob);
      }
    }
    const data = {
      parentJob,
      subJobs
    };
    res.json({ success: true, data: data });
  })));
}